Перейти к основному содержимому

7.05. Справочник по GraphQL

Разработчику Архитектору Инженеру

Справочник по GraphQL


📘 1. Типы в GraphQL (Types)

1.1. Скалярные типы (Scalar Types)

Стандартные (built-in scalars)

ИмяОписаниеПримеры допустимых значенийПримечания
Int32-битное целое со знаком42, -7, 0Диапазон: от −2³¹ до 2³¹−1 (−2147483648 … 2147483647). Не поддерживает Infinity, NaN.
Float64-битное число с плавающей точкой (IEEE 754 double)3.14, -0.001, 2.998e8Поддерживает Infinity, -Infinity, NaNно большинство реализаций сервера не рекомендуют их передачу (не сериализуются в JSON без кастомного скаляра).
StringUTF-8 строка"hello", "", "\\n"Поддерживает escape-последовательности: \", \\, \/, \b, \f, \n, \r, \t, \uXXXX.
BooleanЛогическое значениеtrue, falseСтрочные, без кавычек.
IDУникальный идентификатор (сериализуется как String, но семантически уникален и неинтерпретируем)"abc123", "100"Используется для кэширования, refetching. Может быть числом, но сериализуется как строка.

Пользовательские скаляры (Custom Scalars)

Определяются через scalar <Name>. Обязательно должны быть реализованы:

  • serialize(value) — преобразует внутреннее значение в JSON-совместимое (для ответа).
  • parseValue(value) — преобразует значение из AST (аргументов переменных).
  • parseLiteral(ast) — преобразует значение из AST (литералов в запросе).

Примеры распространённых кастомных скаляров:

  • DateTime (ISO 8601)
  • JSON (произвольные JSON-объекты/массивы)
  • Decimal, Long, BigInt
  • URL, Email, UUID

⚠️ В SDL нельзя указать семантику кастомного скаляра — только имя. Спецификации поведения передаются через @specifiedBy(url: "...").


1.2. Составные типы (Composite Types)

1.2.1. Object

type User {
id: ID!
name: String
email: String @deprecated(reason: "Use contact instead")
contact: ContactType
posts(limit: Int = 10): [Post!]!
}

Свойства объекта как типа:

  • Имя: Userдолжно начинаться с заглавной буквы (по соглашению, не по строгой спецификации, но валидаторы могут ругаться).
  • Поля: не пустой набор полей (минимум 1).
  • Поле состоит из:
    • Имени (id, name и т.д.)
    • Типа возврата (обязательно)
    • Аргументов (опционально)
    • Директив (опционально)

Ограничения:

  • Имя поля должно соответствовать /[_A-Za-z][_0-9A-Za-z]*/.
  • Имена полей уникальны.
  • Циклические зависимости в типах разрешены (например, User → [Post!], Post → author: User).

1.2.2. Input Object

input UserInput {
name: String!
email: String
tags: [String!] = []
}

Особенности:

  • Все поля должны иметь тип (не могут быть интерфейсами или юнионами).
  • Поддерживает значения по умолчанию (только для скаляров и ENUM, массивов/вложенных input-объектов — через литералы).
  • Примеры валидных значений по умолчанию:
    • = "default"
    • = 42
    • = true
    • = []
    • = ["a", "b"]
    • = { name: "anon" } — только если вложенный input тоже поддерживает такие литералы.

null не может быть значением по умолчанию в SDL. Если поле nullable, отсутствие значения → null.

1.2.3. Interface

interface Node {
id: ID!
}

type User implements Node {
id: ID!
name: String
}

type Post implements Node {
id: ID!
title: String
}

Требования к реализации (implements):

  • Каждый тип, реализующий интерфейс, обязан содержать все поля интерфейса с точно совпадающими:
    • Именем
    • Аргументами (имена, типы, обязательность, дефолты)
    • Типом возврата (включая !, [])
  • Дополнительные поля разрешены.
  • Поле интерфейса может возвращать тип, реализующий интерфейс (самореференция разрешена).

Выполнение:

  • При запросе к полю интерфейсного типа (например, node(id: "...") { ... }) сервер должен указать __typename, чтобы клиент мог применить фрагменты.
  • __typename — обязательное поле во всех объектах и интерфейсах.

1.2.4. Union

union SearchResult = User | Post | Comment

Ограничения:

  • Состоит только из Object Types (не интерфейсы, не скаляры, не input).
  • Типы в union должны быть уникальны.
  • Не может быть пустым.

Использование в запросе:

{
search(query: "foo") {
__typename
... on User { name }
... on Post { title }
}
}

❗ Нельзя запрашивать поля напрямую из union-типа — только через inline-фрагменты с on.

1.2.5. Enum

enum UserRole {
MEMBER @deprecated
ADMIN
OWNER
}

Свойства:

  • Имя: /[_A-Za-z][_0-9A-Za-z]*/, заглавные — по соглашению.
  • Значения («значения перечисления»): /[_A-Za-z][_0-9A-Za-z]*/.
  • Поддерживает директивы (@deprecated и др.).
  • Сериализуется как строка ("ADMIN"), но в коде сервера — как enum-значение.

⚠️ Значение null не является валидным значением enum. Если нужно — делайте UserRole nullable: role: UserRole.


1.3. Обёрточные типы (Wrapping Types)

СинтаксисЗначениеПримерПримечания
TnullableStringМожет быть null или значением типа T.
T!non-nullString!Обязательно значение: не может быть null. Если resolver возвращает null — ошибка выполнения.
[T]nullable list[String]Может быть null, [null], ["a"], [].
[T]!non-null list[String]!Список обязателен, но элементы могут быть null: [null, "a"] — валидно.
[T!]nullable list of non-null[String!]Список может быть null, но если не null — элементы не могут: ["a"], [], null — валидны; [null] — ошибка.
[T!]!non-null list of non-null[String!]!Список обязателен, и каждый элемент обязателен: ["a"], [] — валидны; null, [null] — ошибки.

🔍 При валидации аргументов переменных: если переменная объявлена как String!, а в operation variables передано null — ошибка на этапе проверки переменных (до выполнения).


📘 2. Директивы (Directives)

Директивы — это декларативные аннотации в SDL или запросах, влияющие на поведение выполнения или интроспекции. Состоят из @имя(аргументы...).

2.1. Стандартные (built-in) директивы (обязательны в любой реализации)

ДирективаПрименяется кАргументыОписание
@include(if: Boolean!)поля, фрагменты (в запросе)if: Boolean!Включает элемент в запрос, если if == true.
@skip(if: Boolean!)поля, фрагменты (в запросе)if: Boolean!Исключает элемент из запроса, если if == true.
@deprecated(reason: String)поля, аргументы, enum значения (в SDL)reason: String (опциональный)Помечает элемент как устаревший. Появляется в интроспекции через isDeprecated и deprecationReason.
@specifiedBy(url: String!)скалярные типы (в SDL)url: String!Указывает URL спецификации для кастомного скаляра (например, https://www.ietf.org/rfc/rfc3339.txt для DateTime).

⚠️ @include и @skip не могут применяться к аргументам, типам, полям в SDL — только в операциях и фрагментах.

Примеры использования:

query UserQuery($withPosts: Boolean!) {
user(id: "1") {
id
name
posts @include(if: $withPosts) {
title
}
}
}
type User {
id: ID!
legacyEmail: String @deprecated(reason: "Use contact.email instead")
}
scalar DateTime @specifiedBy(url: "https://www.ietf.org/rfc/rfc3339.txt")

2.2. Распространённые кастомные директивы (не входят в спецификацию, но широко используются)

ДирективаКонтекстОписаниеГде встречается
@defer(label: String, if: Boolean)поля (в запросе)Откладывает выполнение поля и возвращает initial response + последующие инкрементальные ответы (Incremental Delivery). Требует поддержки на сервере (Apollo, Envelop, GraphQL-JS ≥16.6).Apollo, Relay, Envelop (spec: RFC)
@stream(initialCount: Int!, label: String, if: Boolean)поля списка ([T])Аналогично @defer, но для потоковой передачи элементов списка по частям.То же, что выше
@link(url: String!, as: String, import: [ImportSpec!], for: LinkPurpose)SDL (в схеме)Объявляет использование спецификации (например, @tag, @shareable, @inaccessible) в Federation 2.Apollo Federation 2
@tag(name: String!)SDL (поля, типы)Помечает элементы для использования в composition (Federation).Federation 2
@shareableSDL (поля интерфейсов и объектов)Разрешает дублирование поля в нескольких подграфах.Federation 2
@inaccessibleSDLСкрывает тип/поле из API, но сохраняет для composition.Federation 2
@override(from: String!)SDLПеренаправляет резолв поля в другой подграф.Federation 2
@composeDirective(name: String!)SDLУказывает, что директива участвует в композиции схем.Federation 2
@cacheControl(maxAge: Int, scope: CacheScope)SDL / запросУправляет кэшированием (Apollo Engine, persisted queries).Apollo Server
@auth(requires: Role!)SDLКонтроль доступа (часто кастомная реализация).Hasura, PostGraphile, Apollo Plugins

🔎 В SDL директивы не изменяют семантику выполнения напрямую — их поведение реализуется сервером. Например, @cacheControl требует подключения apollo-server-plugin-response-cache.


📘 3. Структура документа GraphQL (Document Structure)

Документ (.graphql-файл или строка) — это набор определений. Порядок не важен, но должен соответствовать грамматике.

3.1. Типы определений

ТипSDLЗапросОписание
SchemaDefinitionschema { query: Query, mutation: Mutation } — переопределяет имена корневых типов (по умолчанию Query, Mutation, Subscription).
TypeDefinitiontype, interface, union, scalar, enum, input — объявление типов.
TypeExtensionextend type User { ... } — расширение существующего типа (должен быть объявлен ранее или в другом файле).
DirectiveDefinitiondirective @auth(...) on FIELD_DEFINITION — объявление новой директивы.
OperationDefinitionquery, mutation, subscription — корневой запрос. Может быть именованным или анонимным (но не более одного анонимного в документе).
FragmentDefinitionfragment UserFields on User { ... } — именованный фрагмент.
ExecutableDirectiveДирективы, используемые в запросах (@include, @skip, @defer).

3.2. Корневые операции

schema {
query: RootQuery # по умолчанию — Query
mutation: RootMutation # по умолчанию — Mutation
subscription: Events # по умолчанию — Subscription
}

Правила:

  • queryобязательно (даже если не объявлен явно → подразумевается Query).
  • mutation и subscription — опциональны. Если не объявлены — мутации и подписки запрещены.

📘 4. Аргументы (Arguments)

Аргументы могут быть у:

  • полей объектов и интерфейсов
  • директив (в SDL и запросах)
  • корневых операций (редко, но возможно)

4.1. Типы значений аргументов

КонтекстВозможные значения
В SDL (при объявлении поля)— Тип (обязательно) — Значение по умолчанию (опционально, только для скаляров, ENUM, массивов, input-объектов)
В запросе (литералы)42, "str", true, null, [1,2], { a: "b" }, ENUM_VALUE
В переменныхЛюбое JSON-совместимое значение, соответствующее типу переменной

4.2. Значения по умолчанию в SDL

Поддерживаются следующие литералы:

ТипПримеры
Int / Float= 0, = -42, = 3.14
String= "", = "default"
Boolean= true, = false
Enum= ADMIN, = RED
ID= "default_id"
List= [], = ["a", "b"]
Input Object= { name: "anon", tags: [] }

❗ Нельзя использовать null как значение по умолчанию в SDL. Чтобы поле было null по умолчанию — не делайте его !, и не указывайте дефолт.

4.3. Переменные (Variables)

query GetUser($id: ID!, $limit: Int = 10) {
user(id: $id) {
posts(first: $limit) { id }
}
}

Правила:

  • Имя: $ + /[_A-Za-z][_0-9A-Za-z]*/.
  • Тип переменной не может быть Object, Interface, Union, Input Object без ! или [] — только скаляры, ENUM, input-типы (возможно, обёрнутые).
  • Значения переменных передаются отдельно (в variables: { ... }).
  • Если переменная объявлена как !, но в variablesnull или отсутствует — ошибка валидации до выполнения.

📘 5. Фрагменты (Fragments)

5.1. Именованные фрагменты

fragment UserBase on User {
id
name
}

query {
me { ...UserBase }
other: user(id: "2") { ...UserBase }
}

Особенности:

  • Должен быть объявлен до первого использования в документе (лексический порядок).
  • Может включать другие фрагменты (вложенность разрешена).
  • Используется только на совместимых типах: on User → можно использовать для User, типов, реализующих интерфейс User, или union-типов, содержащих User.

5.2. Inline-фрагменты

{
node(id: "xyz") {
... on User {
name
}
... on Post {
title
}
}
}

Разновидности:

  • ... on Type — условный фрагмент (для union/interface).
  • ... (без on) — «spread all» фрагмент (всё, что совместимо с текущим типом). Например, внутри User:
    {
    me {
    ... # эквивалентно ... on User
    }
    }

⚠️ Бесконечная рекурсия фрагментов (fragment A on T { ...B }, fragment B on T { ...A }) — ошибка валидации.


📘 6. Семантика выполнения (Execution Semantics)

6.1. Резолверы (Resolvers)

Функция-резолвер имеет сигнатуру (в большинстве реализаций):

(parent: any, args: Args, context: Context, info: GraphQLResolveInfo) => any | Promise<any>
  • parent — значение родительского поля (для корневых типов — rootValue, переданный в execute).
  • args — аргументы поля (распарсенные, с применением дефолтов).
  • context — разделяемый объект (аутентификация, БД-соединение и т.д.).
  • info — метаданные: поле, путь, AST, схема.

Правила по умолчанию (если резолвер не задан):

  • Если parent — объект с полем, совпадающим по имени — возвращается parent[fieldName].
  • Если parent[fieldName] — функция без аргументов — вызывается и возвращается результат.
  • Иначе — undefined → интерпретируется как null (если поле nullable) или ошибка (если !).

6.2. Обработка ошибок

  • Ошибка в резолвере (throw) → добавляется в errors ответа, поле становится null (если nullable), иначе — подъём ошибки до ближайшего nullable предка.
  • Системные ошибки (парсинг, валидация, отсутствие резолвера для required поля) — останавливают выполнение до начала резолвинга.

Структура ошибки в ответе:

{
"errors": [
{
"message": "Cannot return null for non-nullable field User.name.",
"locations": [{ "line": 3, "column": 5 }],
"path": ["user", "name"],
"extensions": { ... }
}
],
"data": { "user": null }
}

📘 7. Интроспекция (Introspection)

GraphQL требует поддержки интроспекции — возможности запросить метаданные о самой схеме через стандартные поля и типы. Это основа для инструментов (GraphiQL, Apollo Studio, codegen).

7.1. Специальные поля

ПолеТипНазначение
__typenameString!Возвращает имя типа текущего объекта. Доступно на любом объекте, интерфейсе, union’е. Обязательно для клиентов (например, Apollo Client требует его для normalisation).
__schema__Schema!Возвращает полную схему (только в корне Query).
__type(name: String!)__TypeВозвращает информацию о конкретном типе по имени.

7.2. Стандартные типы интроспекции

__Schema

type __Schema {
description: String # (не в спецификации, но часто добавляется)
types: [__Type!]! # все именованные типы
queryType: __Type! # корневой тип Query
mutationType: __Type # nullable
subscriptionType: __Type # nullable
directives: [__Directive!]!
}

__Type

type __Type {
kind: __TypeKind!
name: String
description: String

# Для OBJECT, INTERFACE, INPUT_OBJECT
fields(includeDeprecated: Boolean = false): [__Field!]

# Для OBJECT, INTERFACE
interfaces: [__Type!]

# Для OBJECT, UNION
possibleTypes: [__Type!]

# Для ENUM
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]

# Для INPUT_OBJECT
inputFields: [__InputValue!]

# Для INTERFACE, UNION, ENUM, INPUT_OBJECT
ofType: __Type

# Federation: используется в composition
specifiedByUrl: String
isOneOf: Boolean # для input объектов с @oneOf (Federation 2.3+)
}

__TypeKind (enum)

ЗначениеОписание
SCALARInt, String, DateTime
OBJECTUser, Post
INTERFACENode, SearchResult
UNION`SearchResult = User
ENUMUserRole
INPUT_OBJECTUserInput
LIST[String] — не именованный, ofType указывает на элемент
NON_NULLString! — не именованный, ofType указывает на обёртываемый тип

__Field

type __Field {
name: String!
description: String
args: [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}

__InputValue

type __InputValue {
name: String!
description: String
type: __Type!
defaultValue: String # сериализованное значение (например, "10", "\"default\"")
isDeprecated: Boolean!
deprecationReason: String
}

🔎 defaultValue — строка, содержащая SDL-литерал. Парсится клиентом по тем же правилам, что и аргументы. Например: "[]", "{ name: \"anon\" }".

__Directive

type __Directive {
name: String!
description: String
locations: [__DirectiveLocation!]!
args: [__InputValue!]!
isRepeatable: Boolean! # можно ли использовать директиву несколько раз на одном элементе
}

__DirectiveLocation (enum)

ГруппаЗначения
ExecutableQUERY, MUTATION, SUBSCRIPTION, FIELD, FRAGMENT_DEFINITION, FRAGMENT_SPREAD, INLINE_FRAGMENT, VARIABLE_DEFINITION
Type SystemSCHEMA, SCALAR, OBJECT, FIELD_DEFINITION, ARGUMENT_DEFINITION, INTERFACE, UNION, ENUM, ENUM_VALUE, INPUT_OBJECT, INPUT_FIELD_DEFINITION

💡 isRepeatable: true позволяет:

type User {
id: ID! @tag(name: "core") @tag(name: "pii")
}

📘 8. Подписки (Subscriptions)

Подписки позволяют серверу отправлять данные клиенту асинхронно в ответ на события.

8.1. Объявление в схеме

type Subscription {
postPublished(topic: String!): Post!
userConnected(id: ID!): UserStatus!
}

8.2. Семантика выполнения

  1. Клиент отправляет subscription { ... }.
  2. Сервер немедленно возвращает data: null, extensions: { ... } и устанавливает подключение (часто через WebSocket).
  3. При возникновении события сервер отправляет инкрементальный ответ:
    { "data": { "postPublished": { "id": "123", "title": "..." } } }
  4. Подписка прекращается при:
    • complete-сообщении от клиента,
    • ошибке,
    • таймауте (опционально),
    • закрытии соединения.

8.3. Протоколы транспорта

ПротоколПоддержкаПримечания
GraphQL over WebSocket (graphql-ws)Современный стандарт (The Guild)Поддерживает @defer, @stream, next/error/complete сообщения. Заменяет subscriptions-transport-ws.
SSE (Server-Sent Events)Простой HTTP-потокТолько от сервера к клиенту. Подходит для read-only подписок. Не поддерживает GQL_START/GQL_STOP — управление через Last-Event-ID.
MQTT/STOMP поверх WebSocketВ enterprise-средахРедко, но используется в IoT или legacy.

⚠️ HTTP long-polling не рекомендуется — нарушает принципы GraphQL (stateless, idempotent).

8.4. Реализация резолвера

Резолвер подписки должен возвращать AsyncIterable (например, AsyncGenerator или EventEmitter-обёртку):

const resolvers = {
Subscription: {
postPublished: {
subscribe: (parent, args, ctx) =>
pubsub.asyncIterator(`POST_PUBLISHED.${args.topic}`),
resolve: (payload, args, ctx) => payload.post, // опционально
},
},
};
  • subscribe() — возвращает AsyncIterable.
  • resolve() — трансформирует payload перед отправкой клиенту (по умолчанию — identity).

📘 9. Federation (Apollo Federation)

Подход к созданию единой GraphQL-схемы из нескольких микросервисов (подграфов).

9.1. Ключевые концепции

ПонятиеОписание
Подграф (Subgraph)Отдельный GraphQL-сервис, управляемый командой. Имеет свою SDL.
SupergraphЕдиная виртуальная схема, сгенерированная композицией подграфов.
CompositionПроцесс объединения SDL подграфов в supergraph (выполняется через rover supergraph compose).
Query PlannerКомпонент routera, разбивающий запрос на подзапросы к подграфам.

9.2. Обязательные директивы Federation 2

ДирективаНазначениеПример
@key(fields: String!)Определяет primary key для типа. Разрешает расширение в других подграфах.type User @key(fields: "id") { id: ID! }
@extendsУказывает, что тип определён в другом подграфе. (Устарел в v2 — достаточно @key.)
@externalПомечает поле как определённое в другом подграфе, но используемое здесь.type Review { user: User! @external }
@requires(fields: String!)Запрашивает поля у родительского типа для резолвинга.user: User @requires(fields: "userId")
@provides(fields: String!)Гарантирует, что поле будет разрешено в текущем подграфе и может использоваться другими.author: User! @provides(fields: "name")

9.3. Пример composition

Подграф users:

type User @key(fields: "id") {
id: ID!
name: String!
}

Подграф posts:

type User @key(fields: "id") {
id: ID!
posts: [Post!]! @shareable
}

type Post @key(fields: "id") {
id: ID!
title: String!
author: User!
}

Supergraph (результат):

type User @key(fields: "id") {
id: ID!
name: String!
posts: [Post!]!
}
type Post { ... }

🔍 @shareable разрешает определение posts в нескольких подграфах (например, для фильтрации по типу).


📘 10. Безопасность и защита

10.1. Ограничение глубины и ширины запроса

  • Depth limiting — ограничение вложенности полей (например, user { friends { friends { ... } } }).
  • Width limiting — ограничение количества полей на уровне.
  • Cost analysis — оценка «стоимости» запроса на основе:
    • сложности поля (1 — скаляр, 10 — список, 100 — внешний вызов),
    • множителя пагинации (first: 1000 → ×1000),
    • кэшируемости.

Реализации: graphql-depth-limit, graphql-cost-analysis, Envelop plugins.

10.2. Persisted Queries

  • Клиент отправляет хэш запроса (например, SHA-256), а не полный текст.
  • Сервер сверяет хэш с предварительно зарегистрированным списком.
  • Преимущества:
    • Защита от injection (нельзя отправить новый запрос),
    • Снижение трафика,
    • Возможность аудита и rate limiting по хэшу.

⚠️ Требует CI/CD-интеграции: rover ingest → отправка SDL + операций в Apollo Studio.

10.3. Rate limiting

  • По operationId (persisted queries),
  • По полю __typename + параметрам (например, user(id: "...")),
  • По IP + токену.

Инструменты: Apollo Studio Embedded Reporting, graphql-rate-limit.


📘 11. Инструменты и подходы к разработке

ПодходОписаниеПлюсыМинусы
SDL-firstСначала пишется .graphql-файл, затем генерируется код (resolvers, types).Чёткий контракт, документированность, codegen.Жёсткая связь, сложнее динамические схемы.
Code-firstСхема строится программно (NestJS, TypeGraphQL, GraphQL-JS).Гибкость, инкапсуляция, TypeScript-first.Риск расхождения кода и документации.
Schema stitchingОбъединение схем через mergeSchemas (устаревшее).Простота для небольших проектов.Слабая изоляция, проблемы с дублированием, нет composition.
FederationСовременный подход к распределённым схемам.Масштабируемость, ownership, incremental adoption.Сложность настройки, требует CI/CD.

📘 12. Рекомендации по проектированию схем

12.1. Именование

КатегорияРекомендация
ТипыPascalCase: UserProfile, PaymentMethod
Поля/аргументыcamelCase: createdAt, maxResults
Enum значенияSCREAMING_SNAKE_CASE: ACTIVE, PENDING_APPROVAL
Input типыСуффикс Input: CreateUserInput, PaginationInput
Payload-типы для мутацийСуффикс Payload: CreateUserPayload { user: User, errors: [Error!] }

12.2. Пагинация

Используйте Relay Cursor Connections Specification:

type UserConnection {
edges: [UserEdge!]!
nodes: [User!]! # удобный alias для edges.node
pageInfo: PageInfo!
}

type UserEdge {
node: User!
cursor: String!
}

type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}

Аргументы: first, after, last, before.

❗ Избегайте offset/limit для больших данных — нет стабильного курсора.

12.3. Версионирование

GraphQL не рекомендует версионирование (/v1/graphql). Вместо этого:

  • Добавляйте поля (бэквард-совместимо),
  • Устаревайте через @deprecated,
  • Используйte @inaccessible в Federation для постепенного удаления.